<?php namespace Illuminate\Database\Eloquent\Relations;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as BaseCollection;

class MorphTo extends BelongsTo {

	/**
	 * The type of the polymorphic relation.
	 *
	 * @var string
	 */
	protected $morphType;

	/**
	 * The models whose relations are being eager loaded.
	 *
	 * @var \Illuminate\Database\Eloquent\Collection
	 */
	protected $models;

	/**
	 * All of the models keyed by ID.
	 *
	 * @var array
	 */
	protected $dictionary = array();

	/*
	 * Indicates if soft-deleted model instances should be fetched.
	 *
	 * @var bool
	 */
	protected $withTrashed = false;

	/**
	 * Create a new morph to relationship instance.
	 *
	 * @param  \Illuminate\Database\Eloquent\Builder  $query
	 * @param  \Illuminate\Database\Eloquent\Model  $parent
	 * @param  string  $foreignKey
	 * @param  string  $otherKey
	 * @param  string  $type
	 * @param  string  $relation
	 * @return void
	 */
	public function __construct(Builder $query, Model $parent, $foreignKey, $otherKey, $type, $relation)
	{
		$this->morphType = $type;

		parent::__construct($query, $parent, $foreignKey, $otherKey, $relation);
	}

	/**
	 * Get the results of the relationship.
	 *
	 * @return mixed
	 */
	public function getResults()
	{
		if ( ! $this->otherKey) return;

		return $this->query->first();
	}

	/**
	 * Set the constraints for an eager load of the relation.
	 *
	 * @param  array  $models
	 * @return void
	 */
	public function addEagerConstraints(array $models)
	{
		$this->buildDictionary($this->models = Collection::make($models));
	}

	/**
	 * Build a dictionary with the models.
	 *
	 * @param  \Illuminate\Database\Eloquent\Collection  $models
	 * @return void
	 */
	protected function buildDictionary(Collection $models)
	{
		foreach ($models as $model)
		{
			if ($model->{$this->morphType})
			{
				$this->dictionary[$model->{$this->morphType}][$model->{$this->foreignKey}][] = $model;
			}
		}
	}

	/**
	 * Match the eagerly loaded results to their parents.
	 *
	 * @param  array   $models
	 * @param  \Illuminate\Database\Eloquent\Collection  $results
	 * @param  string  $relation
	 * @return array
	 */
	public function match(array $models, Collection $results, $relation)
	{
		return $models;
	}

	/**
	 * Associate the model instance to the given parent.
	 *
	 * @param  \Illuminate\Database\Eloquent\Model  $model
	 * @return \Illuminate\Database\Eloquent\Model
	 */
	public function associate(Model $model)
	{
		$this->parent->setAttribute($this->foreignKey, $model->getKey());

		$this->parent->setAttribute($this->morphType, $model->getMorphClass());

		return $this->parent->setRelation($this->relation, $model);
	}

	/**
	 * Dissociate previously associated model from the given parent.
	 *
	 * @return \Illuminate\Database\Eloquent\Model
	 */
	public function dissociate()
	{
		$this->parent->setAttribute($this->foreignKey, null);

		$this->parent->setAttribute($this->morphType, null);

		return $this->parent->setRelation($this->relation, null);
	}

	/**
	 * Get the results of the relationship.
	 *
	 * Called via eager load method of Eloquent query builder.
	 *
	 * @return mixed
	 */
	public function getEager()
	{
		foreach (array_keys($this->dictionary) as $type)
		{
			$this->matchToMorphParents($type, $this->getResultsByType($type));
		}

		return $this->models;
	}

	/**
	 * Match the results for a given type to their parents.
	 *
	 * @param  string  $type
	 * @param  \Illuminate\Database\Eloquent\Collection  $results
	 * @return void
	 */
	protected function matchToMorphParents($type, Collection $results)
	{
		foreach ($results as $result)
		{
			if (isset($this->dictionary[$type][$result->getKey()]))
			{
				foreach ($this->dictionary[$type][$result->getKey()] as $model)
				{
					$model->setRelation($this->relation, $result);
				}
			}
		}
	}

	/**
	 * Get all of the relation results for a type.
	 *
	 * @param  string  $type
	 * @return \Illuminate\Database\Eloquent\Collection
	 */
	protected function getResultsByType($type)
	{
		$instance = $this->createModelByType($type);

		$key = $instance->getKeyName();

		$query = $instance->newQuery();

		$query = $this->useWithTrashed($query);

		return $query->whereIn($key, $this->gatherKeysByType($type)->all())->get();
	}

	/**
	 * Gather all of the foreign keys for a given type.
	 *
	 * @param  string  $type
	 * @return array
	 */
	protected function gatherKeysByType($type)
	{
		$foreign = $this->foreignKey;

		return BaseCollection::make($this->dictionary[$type])->map(function($models) use ($foreign)
		{
			return head($models)->{$foreign};

		})->unique();
	}

	/**
	 * Create a new model instance by type.
	 *
	 * @param  string  $type
	 * @return \Illuminate\Database\Eloquent\Model
	 */
	public function createModelByType($type)
	{
		return new $type;
	}

	/**
	 * Get the foreign key "type" name.
	 *
	 * @return string
	 */
	public function getMorphType()
	{
		return $this->morphType;
	}

	/**
	 * Get the dictionary used by the relationship.
	 *
	 * @return array
	 */
	public function getDictionary()
	{
		return $this->dictionary;
	}

	/**
	 * Fetch soft-deleted model instances with query.
	 *
	 * @return $this
	 */
	public function withTrashed()
	{
		$this->withTrashed = true;

		$this->query = $this->useWithTrashed($this->query);

		return $this;
	}

	/**
	 * Return trashed models with query if told so.
	 *
	 * @param  \Illuminate\Database\Eloquent\Builder  $query
	 * @return \Illuminate\Database\Eloquent\Builder
	 */
	protected function useWithTrashed(Builder $query)
	{
		if ($this->withTrashed && $query->getMacro('withTrashed') !== null)
		{
			return $query->withTrashed();
		}

		return $query;
	}

}
